4-6 支付通知与AES-256-GCM密文解密
支付通知机制
用户完成支付后,微信服务器会向商户配置的 notify_url 发送 POST 请求,通知中包含加密的支付结果。
通知数据结构
{
"id": "通知的唯一ID",
"create_time": "通知创建的时间",
"resource_type": "encrypt-resource",
"event_type": "TRANSACTION.SUCCESS",
"resource": {
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "Base64编码的密文",
"nonce": "随机串",
"associated_data": "附加数据"
}
}
json
| 字段 | 说明 |
|---|---|
event_type | 事件类型:TRANSACTION.SUCCESS(支付成功)、TRANSACTION.CLOSED(关闭)等 |
resource.algorithm | 加密算法,固定为 AEAD_AES_256_GCM |
resource.ciphertext | Base64 编码的密文,需要解密才能读取 |
resource.nonce | 解密所需的随机串 |
resource.associated_data | 解密所需的附加数据 |
AES-256-GCM 解密实现
解密方法
import * as crypto from 'crypto';
/**
* AES-256-GCM 解密微信支付通知密文
* @param ciphertext Base64 编码的密文
* @param nonce 随机串
* @param associatedData 附加数据
* @returns 解密后的 JSON 字符串
*/
private decryptApiV3(
ciphertext: string,
nonce: string,
associatedData: string,
): string {
const apiV3Key = this.configService.get<string>('WECHAT_APIV3_KEY');
// 密文 Base64 解码
const ciphertextBuffer = Buffer.from(ciphertext, 'base64');
// AES-256-GCM 解密
const authTag = ciphertextBuffer.subarray(ciphertextBuffer.length - 16);
const data = ciphertextBuffer.subarray(0, ciphertextBuffer.length - 16);
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
apiV3Key!, // APIv3 密钥(32字节)
nonce, // 初始化向量
);
decipher.setAuthTag(authTag);
decipher.setAAD(Buffer.from(associatedData));
const decrypted = Buffer.concat([
decipher.update(data),
decipher.final(),
]);
return decrypted.toString('utf-8');
}
typescript
解密参数说明
| 参数 | 来源 | 说明 |
|---|---|---|
apiV3Key | .env 配置 | APIv3 密钥,在商户平台设置,32 字节 |
nonce | 通知中的 resource.nonce | 解密初始化向量 |
associatedData | 通知中的 resource.associated_data | 附加认证数据 |
ciphertext | 通知中的 resource.ciphertext | Base64 编码的密文 |
通知接口类型定义
// 微信支付通知结构
export interface WechatPayNotification {
id: string;
create_time: string;
resource_type: string;
event_type: string;
resource: {
algorithm: string;
ciphertext: string;
nonce: string;
associated_data: string;
};
}
// 解密后的交易数据
export interface TransactionResult {
mchid: string;
appid: string;
out_trade_no: string;
transaction_id: string;
trade_type: string;
trade_state: string;
trade_state_desc: string;
bank_type: string;
attach: string;
success_time: string;
payer: {
openid: string;
};
amount: {
total: number;
payer_total: number;
currency: string;
payer_currency: string;
};
}
typescript
Controller 中处理通知
import { Controller, Post, Body, Logger } from '@nestjs/common';
import { PayService } from './pay.service';
import { WechatPayNotification } from './pay.interface';
@Controller('pay')
export class PayController {
private readonly logger = new Logger(PayController.name);
constructor(private readonly payService: PayService) {}
@Post('order')
createOrder(@Body() dto: CreateWechatOrderDto) {
return this.payService.wechatJsPay(dto);
}
@Post('notify')
async notify(@Body() body: WechatPayNotification) {
this.logger.log('收到支付通知', JSON.stringify(body));
try {
// 解密通知密文
const result = this.payService.decryptApiV3(
body.resource.ciphertext,
body.resource.nonce,
body.resource.associated_data,
);
const transaction: TransactionResult = JSON.parse(result);
this.logger.log('解密后的交易数据', JSON.stringify(transaction, null, 2));
// TODO: 根据交易状态更新数据库订单
if (transaction.trade_state === 'SUCCESS') {
this.logger.log(`订单 ${transaction.out_trade_no} 支付成功`);
// 更新订单状态、记录 transaction_id 等
}
return 'OK'; // 告知微信已收到通知
} catch (error) {
this.logger.error('解密支付通知失败', error.message);
return 'OK'; // 仍然返回 OK,避免微信重复通知
}
}
}
typescript
解密后的交易数据示例
{
"mchid": "1234567890",
"appid": "wx1234567890",
"out_trade_no": "20260518143015001_2345678901",
"transaction_id": "4200001234202605181234567890",
"trade_type": "JSAPI",
"trade_state": "SUCCESS",
"trade_state_desc": "支付成功",
"bank_type": "CMB_DEBIT",
"success_time": "2026-05-18T14:30:30+08:00",
"payer": {
"openid": "user_openid_here"
},
"amount": {
"total": 1,
"payer_total": 1,
"currency": "CNY",
"payer_currency": "CNY"
}
}
json
.env 配置补充
# APIv3 密钥(用于解密通知密文)
WECHAT_APIV3_KEY=your_32_char_apiv3_key_here
bash
通知处理注意事项
| 注意点 | 说明 |
|---|---|
| 必须返回 OK | 微信根据返回内容判断通知是否送达 |
| 重复通知 | 微信可能多次发送同一通知,需做幂等处理 |
| 通知验签 | 生产环境应验证通知签名,确认来源可信 |
| 超时重试 | 微信在 15s 内未收到响应会重试,最多重试 10 次 |
| 解密失败 | 检查 APIv3 密钥是否正确(32 字符) |
参考资源
- 微信支付通知 - 支付结果通知文档
- AES-256-GCM - 加密算法规范
- Node.js crypto.createDecipheriv - 解密 API
↑